diff --git a/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js b/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js new file mode 100644 index 0000000000..80ba348b41 --- /dev/null +++ b/awx/ui/client/src/activity-stream/factories/build-anchor.factory.js @@ -0,0 +1,78 @@ +export default + function BuildAnchor($log, $filter) { + // Returns a full resource_name HTML string if link can be derived from supplied context + // returns name of resource if activity stream object doesn't contain enough data to build a UI url + // arguments are: a summary_field object, a resource type, an activity stream object + return function (obj, resource, activity) { + var url = '/#/'; + // try/except pattern asserts that: + // if we encounter a case where a UI url can't or shouldn't be generated, just supply the name of the resource + try { + // catch-all case to avoid generating urls if a resource has been deleted + // if a resource still exists, it'll be serialized in the activity's summary_fields + if (!activity.summary_fields[resource]){ + throw {name : 'ResourceDeleted', message: 'The referenced resource no longer exists'}; + } + switch (resource) { + case 'custom_inventory_script': + url += 'inventory_scripts/' + obj.id + '/'; + break; + case 'group': + if (activity.operation === 'create' || activity.operation === 'delete'){ + // the API formats the changes.inventory field as str 'myInventoryName-PrimaryKey' + var inventory_id = _.last(activity.changes.inventory.split('-')); + url += 'inventories/' + inventory_id + '/manage?group=' + activity.changes.id; + } + else { + url += 'inventories/' + activity.summary_fields.inventory[0].id + '/manage?group=' + (activity.changes.id || activity.changes.object1_pk); + } + break; + case 'host': + url += 'home/hosts/' + obj.id; + break; + case 'job': + url += 'jobs/' + obj.id; + break; + case 'inventory': + url += 'inventories/' + obj.id + '/'; + break; + case 'schedule': + // schedule urls depend on the resource they're associated with + if (activity.summary_fields.job_template){ + url += 'job_templates/' + activity.summary_fields.job_template.id + '/schedules/' + obj.id; + } + else if (activity.summary_fields.project){ + url += 'projects/' + activity.summary_fields.project.id + '/schedules/' + obj.id; + } + else if (activity.summary_fields.system_job_template){ + url += 'management_jobs/' + activity.summary_fields.system_job_template.id + '/schedules/edit/' + obj.id; + } + // urls for inventory sync schedules currently depend on having an inventory id and group id + else { + throw {name : 'NotImplementedError', message : 'activity.summary_fields to build this url not implemented yet'}; + } + break; + case 'notification_template': + url += `notification_templates/${obj.id}`; + break; + case 'role': + throw {name : 'NotImplementedError', message : 'role object management is not consolidated to a single UI view'}; + case 'job_template': + url += `templates/job_template/${obj.id}`; + break; + case 'workflow_job_template': + url += `templates/workflow_job_template/${obj.id}`; + break; + default: + url += resource + 's/' + obj.id + '/'; + } + return ' ' + $filter('sanitize')(obj.name || obj.username) + ' '; + } + catch(err){ + $log.debug(err); + return ' ' + $filter('sanitize')(obj.name || obj.username || '') + ' '; + } + }; + } + + BuildAnchor.$inject = ['$log', '$filter']; diff --git a/awx/ui/client/src/activity-stream/factories/build-description.factory.js b/awx/ui/client/src/activity-stream/factories/build-description.factory.js new file mode 100644 index 0000000000..ecd596dda2 --- /dev/null +++ b/awx/ui/client/src/activity-stream/factories/build-description.factory.js @@ -0,0 +1,126 @@ +export default + function BuildDescription(BuildAnchor, $log, i18n) { + return function (activity) { + + var pastTense = function(operation){ + return (/e$/.test(activity.operation)) ? operation + 'd ' : operation + 'ed '; + }; + // convenience method to see if dis+association operation involves 2 groups + // the group cases are slightly different because groups can be dis+associated into each other + var isGroupRelationship = function(activity){ + return activity.object1 === 'group' && activity.object2 === 'group' && activity.summary_fields.group.length > 1; + }; + + // Activity stream objects will outlive the resources they reference + // in that case, summary_fields will not be available - show generic error text instead + try { + activity.description = pastTense(activity.operation); + switch(activity.object_association){ + // explicit role dis+associations + case 'role': + // object1 field is resource targeted by the dis+association + // object2 field is the resource the role is inherited from + // summary_field.role[0] contains ref info about the role + switch(activity.operation){ + // expected outcome: "disassociated role_name from " + case 'disassociate': + if (isGroupRelationship(activity)){ + activity.description += BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + activity.summary_fields.role[0].role_field + + ' from ' + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); + } + else{ + activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + + ' from ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); + } + break; + // expected outcome: "associated role_name to " + case 'associate': + if (isGroupRelationship(activity)){ + activity.description += BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + activity.summary_fields.role[0].role_field + + ' to ' + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); + } + else{ + activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + + ' to ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); + } + break; + } + break; + // inherited role dis+associations (logic identical to case 'role') + case 'parents': + // object1 field is resource targeted by the dis+association + // object2 field is the resource the role is inherited from + // summary_field.role[0] contains ref info about the role + switch(activity.operation){ + // expected outcome: "disassociated role_name from " + case 'disassociate': + if (isGroupRelationship(activity)){ + activity.description += activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + + 'from ' + activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); + } + else{ + activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + + ' from ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); + } + break; + // expected outcome: "associated role_name to " + case 'associate': + if (isGroupRelationship(activity)){ + activity.description += activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity) + + 'to ' + activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity); + } + else{ + activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + + ' to ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); + } + break; + } + break; + // CRUD operations / resource on resource dis+associations + default: + switch(activity.operation){ + // expected outcome: "disassociated from " + case 'disassociate' : + if (isGroupRelationship(activity)){ + activity.description += activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + + 'from ' + activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); + } + else { + activity.description += activity.object2 + BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + + 'from ' + activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); + } + break; + // expected outcome "associated to " + case 'associate': + // groups are the only resource that can be associated/disassociated into each other + if (isGroupRelationship(activity)){ + activity.description += activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity) + + 'to ' + activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity); + } + else { + activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity) + + 'to ' + activity.object2 + BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity); + } + break; + case 'delete': + activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity); + break; + // expected outcome: "operation " + case 'update': + activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); + break; + case 'create': + activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity); + break; + } + break; + } + } + catch(err){ + $log.debug(err); + activity.description = i18n._('Event summary not available'); + } + }; + } + + BuildDescription.$inject = ['BuildAnchor', '$log', 'i18n']; diff --git a/awx/ui/client/src/activity-stream/factories/show-detail.factory.js b/awx/ui/client/src/activity-stream/factories/show-detail.factory.js new file mode 100644 index 0000000000..fde935e108 --- /dev/null +++ b/awx/ui/client/src/activity-stream/factories/show-detail.factory.js @@ -0,0 +1,39 @@ +export default + function ShowDetail($filter, $rootScope, Rest, Alert, GenerateForm, ProcessErrors, GetBasePath, FormatDate, ActivityDetailForm, Empty, Find) { + return function (params, scope) { + + var activity_id = params.activity_id, + activity = Find({ list: params.scope.activities, key: 'id', val: activity_id }), + element; + + if (activity) { + + // Grab our element out of the dom + element = angular.element(document.getElementById('stream-detail-modal')); + + // Grab the modal's scope so that we can set a few variables + scope = element.scope(); + + scope.changes = activity.changes; + scope.user = ((activity.summary_fields.actor) ? activity.summary_fields.actor.username : 'system') + + ' on ' + $filter('longDate')(activity.timestamp); + scope.operation = activity.description; + scope.header = "Event " + activity.id; + + // Open the modal + $('#stream-detail-modal').modal({ + show: true, + backdrop: 'static', + keyboard: true + }); + + if (!scope.$$phase) { + scope.$digest(); + } + } + + }; + } + + ShowDetail.$inject = ['$filter', '$rootScope', 'Rest', 'Alert', 'GenerateForm', 'ProcessErrors', 'GetBasePath', 'FormatDate', + 'ActivityDetailForm', 'Empty', 'Find']; diff --git a/awx/ui/client/src/activity-stream/factories/stream.factory.js b/awx/ui/client/src/activity-stream/factories/stream.factory.js new file mode 100644 index 0000000000..b3725fc2aa --- /dev/null +++ b/awx/ui/client/src/activity-stream/factories/stream.factory.js @@ -0,0 +1,54 @@ +export default + function Stream($rootScope, $location, $state, Rest, GetBasePath, ProcessErrors, + Wait, StreamList, GenerateList, FormatDate, + BuildDescription, ShowDetail) { + return function (params) { + + var scope = params.scope; + + $rootScope.flashMessage = null; + + // descriptive title describing what AS is showing + scope.streamTitle = (params && params.title) ? params.title : null; + + scope.refreshStream = function () { + $state.go('.', null, {reload: true}); + }; + + scope.showDetail = function (id) { + ShowDetail({ + scope: scope, + activity_id: id + }); + }; + + if(scope.activities && scope.activities.length > 0) { + buildUserAndDescription(); + } + + scope.$watch('activities', function(){ + // Watch for future update to scope.activities (like page change, column sort, search, etc) + buildUserAndDescription(); + }); + + function buildUserAndDescription(){ + scope.activities.forEach(function(activity, i) { + // build activity.user + if (scope.activities[i].summary_fields.actor) { + scope.activities[i].user = "" + + scope.activities[i].summary_fields.actor.username + ""; + } else { + scope.activities[i].user = 'system'; + } + // build description column / action text + BuildDescription(scope.activities[i]); + + }); + } + + }; + } + + Stream.$inject = ['$rootScope', '$location', '$state', 'Rest', 'GetBasePath', + 'ProcessErrors', 'Wait', 'StreamList', 'generateList', 'FormatDate', 'BuildDescription', + 'ShowDetail']; diff --git a/awx/ui/client/src/activity-stream/main.js b/awx/ui/client/src/activity-stream/main.js index 381c421c87..44c82217ab 100644 --- a/awx/ui/client/src/activity-stream/main.js +++ b/awx/ui/client/src/activity-stream/main.js @@ -6,14 +6,20 @@ import activityStreamRoute from './activitystream.route'; import activityStreamController from './activitystream.controller'; - import streamDropdownNav from './streamDropdownNav/stream-dropdown-nav.directive'; - import streamDetailModal from './streamDetailModal/main'; +import BuildAnchor from './factories/build-anchor.factory'; +import BuildDescription from './factories/build-description.factory'; +import ShowDetail from './factories/show-detail.factory'; +import Stream from './factories/stream.factory'; export default angular.module('activityStream', [streamDetailModal.name]) .controller('activityStreamController', activityStreamController) .directive('streamDropdownNav', streamDropdownNav) + .factory('BuildAnchor', BuildAnchor) + .factory('BuildDescription', BuildDescription) + .factory('ShowDetail', ShowDetail) + .factory('Stream', Stream) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(activityStreamRoute); }]); diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index a501bf16ee..7289c1af85 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -39,7 +39,6 @@ if ($basePath) { // Modules import './helpers'; import './lists'; -import './widgets'; import './filters'; import portalMode from './portal-mode/main'; import systemTracking from './system-tracking/main'; @@ -179,7 +178,6 @@ var tower = angular.module('Tower', [ 'md5Helper', 'SelectionHelper', 'HostGroupsFormDefinition', - 'StreamWidget', 'JobsHelper', 'CredentialsHelper', 'StreamListDefinition', diff --git a/awx/ui/client/src/widgets.js b/awx/ui/client/src/widgets.js deleted file mode 100644 index dae0b52e0f..0000000000 --- a/awx/ui/client/src/widgets.js +++ /dev/null @@ -1,11 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import "./widgets/InventorySyncStatus"; -import "./widgets/JobStatus"; -import "./widgets/ObjectCount"; -import "./widgets/SCMSyncStatus"; -import "./widgets/Stream"; \ No newline at end of file diff --git a/awx/ui/client/src/widgets/InventorySyncStatus.js b/awx/ui/client/src/widgets/InventorySyncStatus.js deleted file mode 100644 index 75438f03de..0000000000 --- a/awx/ui/client/src/widgets/InventorySyncStatus.js +++ /dev/null @@ -1,115 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - /** - * @ngdoc function - * @name widgets.function:InventorySyncStatus - * @description - * InventorySyncStatus.js - * - * Dashboard widget showing object counts and license availability. - * - */ - - - -angular.module('InventorySyncStatusWidget', ['RestServices', 'Utilities']) - .factory('InventorySyncStatus', ['$rootScope', '$compile', function ($rootScope, $compile) { - return function (params) { - - var scope = params.scope, - target = params.target, - dashboard = params.dashboard, - html, group_total, group_fail, element, src; - - html = "
\n"; - html += "
Inventory Sync Status
\n"; - html += "
\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - - function makeRow(params) { - var label = params.label, - count = params.count, - fail = params.fail, - link = params.link, - fail_link = params.fail_link, - html = "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - return html; - } - - html += makeRow({ - label: 'Inventories', - count: (dashboard.inventories && dashboard.inventories.total_with_inventory_source) ? - dashboard.inventories.total_with_inventory_source : 0, - fail: (dashboard.inventories && dashboard.inventories.inventory_failed) ? dashboard.inventories.inventory_failed : 0, - link: '/#/inventories/?has_inventory_sources=true', - fail_link: '/#/inventories/?inventory_sources_with_failures=true' - }); - - group_total = 0; - group_fail = 0; - if (dashboard.inventory_sources) { - for (src in dashboard.inventory_sources) { - group_total += (dashboard.inventory_sources[src].total) ? dashboard.inventory_sources[src].total : 0; - group_fail += (dashboard.inventory_sources[src].failed) ? dashboard.inventory_sources[src].failed : 0; - } - } - - html += makeRow({ - label: 'Groups', - count: group_total, - fail: group_fail, - link: '/#/home/groups/?has_external_source=true', - fail_link: '/#/home/groups/?status=failed' - }); - - // Each inventory source - for (src in dashboard.inventory_sources) { - if (dashboard.inventory_sources[src].total) { - html += makeRow({ - label: dashboard.inventory_sources[src].label, - count: (dashboard.inventory_sources[src].total) ? dashboard.inventory_sources[src].total : 0, - fail: (dashboard.inventory_sources[src].failed) ? dashboard.inventory_sources[src].failed : 0, - link: '/#/home/groups/?source=' + src, - fail_link: '/#/home/groups/?status=failed&source=' + src - }); - } - } - - html += "\n"; - html += "
FailedTotal
0) ? 'failed-column' : 'zero-column'; - html += " text-right\">"; - html += "" + fail + ""; - html += ""; - html += "" + count + ""; - html += "
\n"; - html += "
\n"; - html += "
\n"; - html += "\n"; - - element = angular.element(document.getElementById(target)); - element.html(html); - $compile(element)(scope); - scope.$emit('WidgetLoaded'); - - }; - } -]); diff --git a/awx/ui/client/src/widgets/JobStatus.js b/awx/ui/client/src/widgets/JobStatus.js deleted file mode 100644 index 2ec89936a3..0000000000 --- a/awx/ui/client/src/widgets/JobStatus.js +++ /dev/null @@ -1,107 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - /** - * @ngdoc function - * @name widgets.function:JobStatus - * @description -* JobStatus.js -* -* Dashboard widget showing object counts and license availability. -* -*/ - - - -angular.module('JobStatusWidget', ['RestServices', 'Utilities']) - .factory('JobStatus', ['$rootScope', '$compile', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', - function ($rootScope, $compile) { - return function (params) { - - var scope = params.scope, - target = params.target, - dashboard = params.dashboard, - html = '', element; - - html = "
\n"; - html += "
Job Status
\n"; - html += "
\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - - function makeRow(params) { - var html = '', - label = params.label, - link = params.link, - fail_link = params.fail_link, - count = params.count, - fail = params.fail; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - return html; - } - - html += makeRow({ - label: 'Jobs', - link: '/#/jobs', - count: (dashboard.jobs && dashboard.jobs.total) ? dashboard.jobs.total : 0, - fail: (dashboard.jobs && dashboard.jobs.failed) ? dashboard.jobs.failed : 0, - fail_link: '/#/jobs/?status=failed' - }); - html += makeRow({ - label: 'Inventories', - link: '/#/inventories', - count: (dashboard.inventories && dashboard.inventories.total) ? dashboard.inventories.total : 0, - fail: (dashboard.inventories && dashboard.inventories.job_failed) ? dashboard.inventories.job_failed : 0, - fail_link: '/#/inventories/?has_active_failures=true' - }); - html += makeRow({ - label: 'Groups', - link: '/#/home/groups', - count: (dashboard.groups && dashboard.groups.total) ? dashboard.groups.total : 0, - fail: (dashboard.groups && dashboard.groups.job_failed) ? dashboard.groups.job_failed : 0, - fail_link: '/#/home/groups/?has_active_failures=true' - }); - html += makeRow({ - label: 'Hosts', - link: '/#/home/hosts', - count: (dashboard.hosts && dashboard.hosts.total) ? dashboard.hosts.total : 0, - fail: (dashboard.hosts && dashboard.hosts.failed) ? dashboard.hosts.failed : 0, - fail_link: '/#/home/hosts/?has_active_failures=true' - }); - - html += "\n"; - html += "
FailedTotal
0) ? 'failed-column' : 'zero-column'; - html += " text-right\">"; - html += "" + fail + ""; - html += ""; - html += "" + count + ""; - html += "
\n"; - html += "
\n"; - html += "
\n"; - html += "\n"; - - element = angular.element(document.getElementById(target)); - element.html(html); - $compile(element)(scope); - scope.$emit('WidgetLoaded'); - - }; - } - ]); diff --git a/awx/ui/client/src/widgets/ObjectCount.js b/awx/ui/client/src/widgets/ObjectCount.js deleted file mode 100644 index 84ddd6fb80..0000000000 --- a/awx/ui/client/src/widgets/ObjectCount.js +++ /dev/null @@ -1,78 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - /** - * @ngdoc function - * @name widgets.function:ObjectCount - * @description - * ObjectCount.js - * - * Dashboard widget showing object counts and license availability. - * - */ - - - -angular.module('ObjectCountWidget', ['RestServices', 'Utilities']) - .factory('ObjectCount', ['$rootScope', '$compile', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', - function ($rootScope, $compile) { - return function (params) { - - var scope = params.scope, - target = params.target, - dashboard = params.dashboard, - keys = ['organizations', 'users', 'teams', 'credentials', 'projects', 'inventories', 'groups', 'hosts', - 'job_templates', 'jobs' - ], - i, html, element; - - html = "
\n"; - html += "
System Summary
\n"; - html += "
\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - - function makeRow(params) { - var html = '', - label = params.label, - link = params.link, - count = params.count; - html += "\n"; - html += "\n"; - html += "\n"; - return html; - } - - for (i = 0; i < keys.length; i++) { - html += makeRow({ - label: keys[i], - link: '/#/' + ((keys[i] === 'hosts' || keys[i] === 'groups') ? 'home/' + keys[i] : keys[i]), - count: (dashboard[keys[i]] && dashboard[keys[i]].total) ? dashboard[keys[i]].total : 0 - }); - } - - html += "\n"; - html += "
Total
"; - html += "" + count + ""; - html += "
\n"; - html += "
\n"; - html += "
\n"; - element = angular.element(document.getElementById(target)); - element.html(html); - $compile(element)(scope); - scope.$emit('WidgetLoaded'); - }; - } - ]); diff --git a/awx/ui/client/src/widgets/SCMSyncStatus.js b/awx/ui/client/src/widgets/SCMSyncStatus.js deleted file mode 100644 index b4d452f06a..0000000000 --- a/awx/ui/client/src/widgets/SCMSyncStatus.js +++ /dev/null @@ -1,108 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - /** - * @ngdoc function - * @name widgets.function:SCMSyncStatus - * @description - * SCMSyncStatus.js - * - * Dashboard widget showing object counts and license availability. - * - */ - - -angular.module('SCMSyncStatusWidget', ['RestServices', 'Utilities']) - .factory('SCMSyncStatus', ['$rootScope', '$compile', - function ($rootScope, $compile) { - return function (params) { - - var scope = params.scope, - target = params.target, - dashboard = params.dashboard, - i, html, total_count, element, type, labelList; - - html = "
\n"; - html += "
Project SCM Status
\n"; - html += "
\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - - function makeRow(params) { - var html = '', - label = params.label, - link = params.link, - fail_link = params.fail_link, - count = params.count, - fail = params.fail; - html += "\n"; - html += "\n"; - html += "\n"; - html += "\n"; - return html; - } - - total_count = 0; - if (dashboard.scm_types) { - for (type in dashboard.scm_types) { - total_count += (dashboard.scm_types[type].total) ? dashboard.scm_types[type].total : 0; - } - } - - html += makeRow({ - label: 'Projects', - link: '/#/projects', - count: total_count, - fail: (dashboard.projects && dashboard.projects.failed) ? dashboard.projects.failed : 0, - fail_link: '/#/projects/?status=failed' - }); - - labelList = []; - for (type in dashboard.scm_types) { - labelList.push(type); - } - labelList.sort(); - for (i = 0; i < labelList.length; i++) { - type = labelList[i]; - if (dashboard.scm_types[type].total) { - html += makeRow({ - label: dashboard.scm_types[type].label, - link: '/#/projects/?scm_type=' + type, - count: (dashboard.scm_types[type].total) ? dashboard.scm_types[type].total : 0, - fail: (dashboard.scm_types[type].failed) ? dashboard.scm_types[type].failed : 0, - fail_link: '/#/projects/?scm_type=' + type + '&status=failed' - }); - } - } - - html += "\n"; - html += "
FailedTotal
" + label + " 0) ? 'failed-column' : 'zero-column'; - html += " text-right\">"; - html += "" + fail + ""; - html += ""; - html += "" + count + ""; - html += "
\n"; - html += "
\n"; - html += "
\n"; - html += "\n"; - - element = angular.element(document.getElementById(target)); - element.html(html); - $compile(element)(scope); - scope.$emit('WidgetLoaded'); - - }; - } - ]); diff --git a/awx/ui/client/src/widgets/Stream.js b/awx/ui/client/src/widgets/Stream.js deleted file mode 100644 index 64315e7953..0000000000 --- a/awx/ui/client/src/widgets/Stream.js +++ /dev/null @@ -1,317 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - /** - * @ngdoc function - * @name widgets.function:Stream - * @description - * Stream.js - * - * Activity stream widget that can be called from anywhere - * - */ - -import listGenerator from '../shared/list-generator/main'; - - -angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefinition', listGenerator.name, 'StreamWidget', -]) - -.factory('BuildAnchor', [ '$log', '$filter', - // Returns a full resource_name HTML string if link can be derived from supplied context - // returns name of resource if activity stream object doesn't contain enough data to build a UI url - // arguments are: a summary_field object, a resource type, an activity stream object - function ($log, $filter) { - return function (obj, resource, activity) { - var url = '/#/'; - // try/except pattern asserts that: - // if we encounter a case where a UI url can't or shouldn't be generated, just supply the name of the resource - try { - // catch-all case to avoid generating urls if a resource has been deleted - // if a resource still exists, it'll be serialized in the activity's summary_fields - if (!activity.summary_fields[resource]){ - throw {name : 'ResourceDeleted', message: 'The referenced resource no longer exists'}; - } - switch (resource) { - case 'custom_inventory_script': - url += 'inventory_scripts/' + obj.id + '/'; - break; - case 'group': - if (activity.operation === 'create' || activity.operation === 'delete'){ - // the API formats the changes.inventory field as str 'myInventoryName-PrimaryKey' - var inventory_id = _.last(activity.changes.inventory.split('-')); - url += 'inventories/' + inventory_id + '/manage?group=' + activity.changes.id; - } - else { - url += 'inventories/' + activity.summary_fields.inventory[0].id + '/manage?group=' + (activity.changes.id || activity.changes.object1_pk); - } - break; - case 'host': - url += 'home/hosts/' + obj.id; - break; - case 'job': - url += 'jobs/' + obj.id; - break; - case 'inventory': - url += 'inventories/' + obj.id + '/'; - break; - case 'schedule': - // schedule urls depend on the resource they're associated with - if (activity.summary_fields.job_template){ - url += 'job_templates/' + activity.summary_fields.job_template.id + '/schedules/' + obj.id; - } - else if (activity.summary_fields.project){ - url += 'projects/' + activity.summary_fields.project.id + '/schedules/' + obj.id; - } - else if (activity.summary_fields.system_job_template){ - url += 'management_jobs/' + activity.summary_fields.system_job_template.id + '/schedules/edit/' + obj.id; - } - // urls for inventory sync schedules currently depend on having an inventory id and group id - else { - throw {name : 'NotImplementedError', message : 'activity.summary_fields to build this url not implemented yet'}; - } - break; - case 'notification_template': - url += `notification_templates/${obj.id}`; - break; - case 'role': - throw {name : 'NotImplementedError', message : 'role object management is not consolidated to a single UI view'}; - case 'job_template': - url += `templates/job_template/${obj.id}`; - break; - case 'workflow_job_template': - url += `templates/workflow_job_template/${obj.id}`; - break; - default: - url += resource + 's/' + obj.id + '/'; - } - return ' ' + $filter('sanitize')(obj.name || obj.username) + ' '; - } - catch(err){ - $log.debug(err); - return ' ' + $filter('sanitize')(obj.name || obj.username || '') + ' '; - } - }; - } -]) - -.factory('BuildDescription', ['BuildAnchor', '$log', 'i18n', - function (BuildAnchor, $log, i18n) { - return function (activity) { - - var pastTense = function(operation){ - return (/e$/.test(activity.operation)) ? operation + 'd ' : operation + 'ed '; - }; - // convenience method to see if dis+association operation involves 2 groups - // the group cases are slightly different because groups can be dis+associated into each other - var isGroupRelationship = function(activity){ - return activity.object1 === 'group' && activity.object2 === 'group' && activity.summary_fields.group.length > 1; - }; - - // Activity stream objects will outlive the resources they reference - // in that case, summary_fields will not be available - show generic error text instead - try { - activity.description = pastTense(activity.operation); - switch(activity.object_association){ - // explicit role dis+associations - case 'role': - // object1 field is resource targeted by the dis+association - // object2 field is the resource the role is inherited from - // summary_field.role[0] contains ref info about the role - switch(activity.operation){ - // expected outcome: "disassociated role_name from " - case 'disassociate': - if (isGroupRelationship(activity)){ - activity.description += BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + activity.summary_fields.role[0].role_field + - ' from ' + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); - } - else{ - activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + - ' from ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); - } - break; - // expected outcome: "associated role_name to " - case 'associate': - if (isGroupRelationship(activity)){ - activity.description += BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + activity.summary_fields.role[0].role_field + - ' to ' + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); - } - else{ - activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + - ' to ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); - } - break; - } - break; - // inherited role dis+associations (logic identical to case 'role') - case 'parents': - // object1 field is resource targeted by the dis+association - // object2 field is the resource the role is inherited from - // summary_field.role[0] contains ref info about the role - switch(activity.operation){ - // expected outcome: "disassociated role_name from " - case 'disassociate': - if (isGroupRelationship(activity)){ - activity.description += activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + - 'from ' + activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); - } - else{ - activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + - ' from ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); - } - break; - // expected outcome: "associated role_name to " - case 'associate': - if (isGroupRelationship(activity)){ - activity.description += activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity) + - 'to ' + activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity); - } - else{ - activity.description += BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + activity.summary_fields.role[0].role_field + - ' to ' + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); - } - break; - } - break; - // CRUD operations / resource on resource dis+associations - default: - switch(activity.operation){ - // expected outcome: "disassociated from " - case 'disassociate' : - if (isGroupRelationship(activity)){ - activity.description += activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity) + - 'from ' + activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity); - } - else { - activity.description += activity.object2 + BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity) + - 'from ' + activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); - } - break; - // expected outcome "associated to " - case 'associate': - // groups are the only resource that can be associated/disassociated into each other - if (isGroupRelationship(activity)){ - activity.description += activity.object1 + BuildAnchor(activity.summary_fields.group[0], activity.object1, activity) + - 'to ' + activity.object2 + BuildAnchor(activity.summary_fields.group[1], activity.object2, activity); - } - else { - activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity) + - 'to ' + activity.object2 + BuildAnchor(activity.summary_fields[activity.object2][0], activity.object2, activity); - } - break; - case 'delete': - activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity); - break; - // expected outcome: "operation " - case 'update': - activity.description += activity.object1 + BuildAnchor(activity.summary_fields[activity.object1][0], activity.object1, activity); - break; - case 'create': - activity.description += activity.object1 + BuildAnchor(activity.changes, activity.object1, activity); - break; - } - break; - } - } - catch(err){ - $log.debug(err); - activity.description = i18n._('Event summary not available'); - } - }; - } -]) - -.factory('ShowDetail', ['$filter', '$rootScope', 'Rest', 'Alert', 'GenerateForm', 'ProcessErrors', 'GetBasePath', 'FormatDate', - 'ActivityDetailForm', 'Empty', 'Find', - function ($filter, $rootScope, Rest, Alert, GenerateForm, ProcessErrors, GetBasePath, FormatDate, ActivityDetailForm, Empty, Find) { - return function (params, scope) { - - var activity_id = params.activity_id, - activity = Find({ list: params.scope.activities, key: 'id', val: activity_id }), - element; - - if (activity) { - - // Grab our element out of the dom - element = angular.element(document.getElementById('stream-detail-modal')); - - // Grab the modal's scope so that we can set a few variables - scope = element.scope(); - - scope.changes = activity.changes; - scope.user = ((activity.summary_fields.actor) ? activity.summary_fields.actor.username : 'system') + - ' on ' + $filter('longDate')(activity.timestamp); - scope.operation = activity.description; - scope.header = "Event " + activity.id; - - // Open the modal - $('#stream-detail-modal').modal({ - show: true, - backdrop: 'static', - keyboard: true - }); - - if (!scope.$$phase) { - scope.$digest(); - } - } - }; - } -]) - -.factory('Stream', ['$rootScope', '$location', '$state', 'Rest', 'GetBasePath', - 'ProcessErrors', 'Wait', 'StreamList', 'generateList', 'FormatDate', 'BuildDescription', - 'ShowDetail', - function ($rootScope, $location, $state, Rest, GetBasePath, ProcessErrors, - Wait, StreamList, GenerateList, FormatDate, - BuildDescription, ShowDetail) { - return function (params) { - - var scope = params.scope; - - $rootScope.flashMessage = null; - - // descriptive title describing what AS is showing - scope.streamTitle = (params && params.title) ? params.title : null; - - scope.refreshStream = function () { - $state.go('.', null, {reload: true}); - }; - - scope.showDetail = function (id) { - ShowDetail({ - scope: scope, - activity_id: id - }); - }; - - if(scope.activities && scope.activities.length > 0) { - buildUserAndDescription(); - } - - scope.$watch('activities', function(){ - // Watch for future update to scope.activities (like page change, column sort, search, etc) - buildUserAndDescription(); - }); - - function buildUserAndDescription(){ - scope.activities.forEach(function(activity, i) { - // build activity.user - if (scope.activities[i].summary_fields.actor) { - scope.activities[i].user = "" + - scope.activities[i].summary_fields.actor.username + ""; - } else { - scope.activities[i].user = 'system'; - } - // build description column / action text - BuildDescription(scope.activities[i]); - - }); - } - }; - } -]);