Merge pull request #5599 from mabashian/cleanup-widgets

Cleanup widgets
This commit is contained in:
Michael Abashian 2017-03-01 14:25:32 -05:00 committed by GitHub
commit 6604a36bc9
12 changed files with 305 additions and 740 deletions

View File

@ -0,0 +1,78 @@
export default
function BuildAnchor($log, $filter) {
// Returns a full <a href=''>resource_name</a> 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 ' <a href=\"' + url + '\"> ' + $filter('sanitize')(obj.name || obj.username) + ' </a> ';
}
catch(err){
$log.debug(err);
return ' ' + $filter('sanitize')(obj.name || obj.username || '') + ' ';
}
};
}
BuildAnchor.$inject = ['$log', '$filter'];

View File

@ -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 <object2> role_name from <object1>"
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 <object2> role_name to <object1>"
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 <object2> role_name from <object1>"
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 <object2> role_name to <object1>"
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 <object2> from <object1>"
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 <object2> to <object1>"
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 <object1>"
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'];

View File

@ -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'];

View File

@ -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 = "<a href=\"/#/users/" + scope.activities[i].summary_fields.actor.id + "\">" +
scope.activities[i].summary_fields.actor.username + "</a>";
} 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'];

View File

@ -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);
}]);

View File

@ -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',

View File

@ -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";

View File

@ -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 = "<div class=\"panel panel-default\">\n";
html += "<div class=\"panel-heading\">Inventory Sync Status</div>\n";
html += "<div class=\"panel-body\">\n";
html += "<table class=\"table table-condensed table-hover\">\n";
html += "<thead>\n";
html += "<tr>\n";
html += "<th class=\"col-md-4 col-lg-3\"></th>\n";
html += "<th class=\"col-md-2 col-lg-1 text-right\">Failed</th>\n";
html += "<th class=\"col-md-2 col-lg-1 text-right\">Total</th>\n";
html += "</tr>\n";
html += "</thead>\n";
html += "<tbody>\n";
function makeRow(params) {
var label = params.label,
count = params.count,
fail = params.fail,
link = params.link,
fail_link = params.fail_link,
html = "<tr>\n";
html += "<td><a href=\"" + link + "\"";
html += (label === 'Hosts' || label === 'Groups') ? " class=\"pad-left-sm\" " : "";
html += ">" + label + "</a></td>\n";
html += "<td class=\"";
html += (fail > 0) ? 'failed-column' : 'zero-column';
html += " text-right\">";
html += "<a href=\"" + fail_link + "\">" + fail + "</a>";
html += "</td>\n";
html += "<td class=\"text-right\">";
html += "<a href=\"" + link + "\">" + count + "</a>";
html += "</td></tr>\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 += "</tbody>\n";
html += "</table>\n";
html += "</div>\n";
html += "</div>\n";
html += "</div>\n";
element = angular.element(document.getElementById(target));
element.html(html);
$compile(element)(scope);
scope.$emit('WidgetLoaded');
};
}
]);

View File

@ -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 = "<div class=\"panel panel-default\">\n";
html += "<div class=\"panel-heading\">Job Status</div>\n";
html += "<div class=\"panel-body\">\n";
html += "<table class=\"table table-condensed table-hover\">\n";
html += "<thead>\n";
html += "<tr>\n";
html += "<th class=\"col-md-4 col-lg-3\"></th>\n";
html += "<th class=\"col-md-2 col-lg-1 text-right\">Failed</th>\n";
html += "<th class=\"col-md-2 col-lg-1 text-right\">Total</th>\n";
html += "</tr>\n";
html += "</thead>\n";
html += "<tbody>\n";
function makeRow(params) {
var html = '',
label = params.label,
link = params.link,
fail_link = params.fail_link,
count = params.count,
fail = params.fail;
html += "<tr>\n";
html += "<td><a href=\"" + link + "\"";
html += (label === 'Hosts' || label === 'Groups') ? " class=\"pad-left-sm\" " : "";
html += ">" + label + "</a></td>\n";
html += "<td class=\"";
html += (fail > 0) ? 'failed-column' : 'zero-column';
html += " text-right\">";
html += "<a href=\"" + fail_link + "\">" + fail + "</a>";
html += "</td>\n";
html += "<td class=\"text-right\">";
html += "<a href=\"" + link + "\" >" + count + "</a>";
html += "</td></tr>\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 += "</tbody>\n";
html += "</table>\n";
html += "</div>\n";
html += "</div>\n";
html += "</div>\n";
element = angular.element(document.getElementById(target));
element.html(html);
$compile(element)(scope);
scope.$emit('WidgetLoaded');
};
}
]);

View File

@ -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 = "<div class=\"panel panel-default\">\n";
html += "<div class=\"panel-heading\">System Summary</div>\n";
html += "<div class=\"panel-body\">\n";
html += "<table class=\"table table-condensed table-hover\">\n";
html += "<thead>\n";
html += "<tr>\n";
html += "<th class=\"col-md-5 col-lg-4\"></th>\n";
html += "<th class=\"col-md-1 col-lg-1 text-right\">Total</th>\n";
html += "</tr>\n";
html += "</thead>\n";
html += "<tbody>\n";
function makeRow(params) {
var html = '',
label = params.label,
link = params.link,
count = params.count;
html += "<tr>\n";
html += "<td class=\"capitalize\"><a href=\"" + link + "\"";
html += (label === 'hosts' || label === 'groups') ? " class=\"pad-left-sm\" " : "";
html += ">" + label.replace(/\_/g, ' ') + "</a></td>\n";
html += "<td class=\"text-right\">";
html += "<a href=\"" + link + "\" >" + count + "</a>";
html += "</td></tr>\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 += "</tbody>\n";
html += "</table>\n";
html += "</div>\n";
html += "</div>\n";
element = angular.element(document.getElementById(target));
element.html(html);
$compile(element)(scope);
scope.$emit('WidgetLoaded');
};
}
]);

View File

@ -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 = "<div class=\"panel panel-default\">\n";
html += "<div class=\"panel-heading\">Project SCM Status</div>\n";
html += "<div class=\"panel-body\">\n";
html += "<table class=\"table table-condensed table-hover\">\n";
html += "<thead>\n";
html += "<tr>\n";
html += "<th class=\"col-md-4 col-lg-3\"></th>\n";
html += "<th class=\"col-md-2 col-lg-1 text-right\">Failed</th>\n";
html += "<th class=\"col-md-2 col-lg-1 text-right\">Total</th>\n";
html += "</tr>\n";
html += "</thead>\n";
html += "<tbody>\n";
function makeRow(params) {
var html = '',
label = params.label,
link = params.link,
fail_link = params.fail_link,
count = params.count,
fail = params.fail;
html += "<tr>\n";
html += "<td><a href=\"" + link + "\">" + label + "</a></td>\n";
html += "<td class=\"";
html += (fail > 0) ? 'failed-column' : 'zero-column';
html += " text-right\">";
html += "<a href=\"" + fail_link + "\">" + fail + "</a>";
html += "</td>\n";
html += "<td class=\"text-right\">";
html += "<a href=\"" + link + "\" >" + count + "</a>";
html += "</td></tr>\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 += "</tbody>\n";
html += "</table>\n";
html += "</div>\n";
html += "</div>\n";
html += "</div>\n";
element = angular.element(document.getElementById(target));
element.html(html);
$compile(element)(scope);
scope.$emit('WidgetLoaded');
};
}
]);

View File

@ -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 <a href=''>resource_name</a> 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 ' <a href=\"' + url + '\"> ' + $filter('sanitize')(obj.name || obj.username) + ' </a> ';
}
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 <object2> role_name from <object1>"
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 <object2> role_name to <object1>"
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 <object2> role_name from <object1>"
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 <object2> role_name to <object1>"
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 <object2> from <object1>"
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 <object2> to <object1>"
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 <object1>"
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 = "<a href=\"/#/users/" + scope.activities[i].summary_fields.actor.id + "\">" +
scope.activities[i].summary_fields.actor.username + "</a>";
} else {
scope.activities[i].user = 'system';
}
// build description column / action text
BuildDescription(scope.activities[i]);
});
}
};
}
]);