diff --git a/awx/ui/static/css/custom-theme/jquery-ui-1.10.3.custom.css b/awx/ui/static/css/custom-theme/jquery-ui-1.10.3.custom.css
index a268ebf601..dbec9aa337 100644
--- a/awx/ui/static/css/custom-theme/jquery-ui-1.10.3.custom.css
+++ b/awx/ui/static/css/custom-theme/jquery-ui-1.10.3.custom.css
@@ -846,7 +846,7 @@ body .ui-tooltip {
.ui-widget-content .ui-state-focus,
.ui-widget-header .ui-state-focus {
border: 1px solid #e3e3e3;
- background: #e5e3e3 url(/static/css/images/ui-bg_flat_75_e5e3e3_40x100.png) 50% 50% repeat-x;
+ background: #e5e3e3 url(/static/css/custom-theme/images/ui-bg_flat_75_e5e3e3_40x100.png) 50% 50% repeat-x;
font-weight: bold;
color: #005580;
}
diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js
index 7752795972..6b48bd8be0 100644
--- a/awx/ui/static/js/app.js
+++ b/awx/ui/static/js/app.js
@@ -84,7 +84,8 @@ angular.module('ansible', [
'TimerService',
'StreamListDefinition',
'HomeGroupListDefinition',
- 'HomeHostListDefinition'
+ 'HomeHostListDefinition',
+ 'ActivityDetailDefinition'
])
.config(['$routeProvider', function($routeProvider) {
$routeProvider.
diff --git a/awx/ui/static/js/controllers/Home.js b/awx/ui/static/js/controllers/Home.js
index 735604c501..ba7a6bc53e 100644
--- a/awx/ui/static/js/controllers/Home.js
+++ b/awx/ui/static/js/controllers/Home.js
@@ -16,28 +16,33 @@ function Home ($routeParams, $scope, $rootScope, $location, Wait, ObjectCount, J
ClearScope('home'); //Garbage collection. Don't leave behind any listeners/watchers from the prior
//scope.
- var waitCount = 4;
- var loadedCount = 0;
-
- if (!$routeParams['login']) {
- // If we're not logging in, start the Wait widget. Otherwise, it's already running.
- Wait('start');
- }
-
- JobStatus({ target: 'container1' });
- InventorySyncStatus({ target: 'container2' });
- SCMSyncStatus({ target: 'container4' });
- ObjectCount({ target: 'container3' });
-
- $rootScope.showActivity = function() { Stream(); }
-
- $rootScope.$on('WidgetLoaded', function() {
- // Once all the widgets report back 'loaded', turn off Wait widget
- loadedCount++;
- if ( loadedCount == waitCount ) {
- Wait('stop');
+ var load = function() {
+ var waitCount = 4;
+ var loadedCount = 0;
+
+ if (!$routeParams['login']) {
+ // If we're not logging in, start the Wait widget. Otherwise, it's already running.
+ Wait('start');
}
- });
+
+ JobStatus({ target: 'container1' });
+ InventorySyncStatus({ target: 'container2' });
+ SCMSyncStatus({ target: 'container4' });
+ ObjectCount({ target: 'container3' });
+
+ $rootScope.$on('WidgetLoaded', function() {
+ // Once all the widgets report back 'loaded', turn off Wait widget
+ loadedCount++;
+ if ( loadedCount == waitCount ) {
+ Wait('stop');
+ }
+ });
+ }
+
+ $rootScope.showActivity = function() { Stream(); }
+ $rootScope.refresh = function() { load(); }
+
+ load();
}
Home.$inject=[ '$routeParams', '$scope', '$rootScope', '$location', 'Wait', 'ObjectCount', 'JobStatus', 'InventorySyncStatus',
@@ -96,6 +101,14 @@ function HomeGroups ($location, $routeParams, HomeGroupList, GenerateList, Proce
PaginateInit({ scope: scope, list: list, url: defaultUrl });
// Process search params
+ if ($routeParams['name']) {
+ scope[list.iterator + 'InputDisable'] = false;
+ scope[list.iterator + 'SearchValue'] = $routeParams['name'];
+ scope[list.iterator + 'SearchField'] = 'name';
+ scope[list.iterator + 'SearchFieldLabel'] = list.fields['name'].label;
+ scope[list.iterator + 'SearchSelectValue'] = null;
+ }
+
if ($routeParams['has_active_failures']) {
scope[list.iterator + 'InputDisable'] = true;
scope[list.iterator + 'SearchValue'] = $routeParams['has_active_failures'];
@@ -180,7 +193,16 @@ function HomeHosts ($location, $routeParams, HomeHostList, GenerateList, Process
SearchInit({ scope: scope, set: 'hosts', list: list, url: defaultUrl });
PaginateInit({ scope: scope, list: list, url: defaultUrl });
-
+
+ // Process search params
+ if ($routeParams['name']) {
+ scope[list.iterator + 'InputDisable'] = false;
+ scope[list.iterator + 'SearchValue'] = $routeParams['name'];
+ scope[list.iterator + 'SearchField'] = 'name';
+ scope[list.iterator + 'SearchFieldLabel'] = list.fields['name'].label;
+ scope[list.iterator + 'SearchSelectValue'] = null;
+ }
+
if ($routeParams['has_active_failures']) {
scope[HomeHostList.iterator + 'InputDisable'] = true;
scope[HomeHostList.iterator + 'SearchValue'] = $routeParams['has_active_failures'];
diff --git a/awx/ui/static/js/controllers/Organizations.js b/awx/ui/static/js/controllers/Organizations.js
index f325f8161f..a64cfc4b61 100644
--- a/awx/ui/static/js/controllers/Organizations.js
+++ b/awx/ui/static/js/controllers/Organizations.js
@@ -12,7 +12,7 @@
function OrganizationsList ($routeParams, $scope, $rootScope, $location, $log, Rest, Alert, LoadBreadCrumbs, Prompt,
GenerateList, OrganizationList, SearchInit, PaginateInit, ClearScope, ProcessErrors,
- GetBasePath, SelectionInit, Wait)
+ GetBasePath, SelectionInit, Wait, Stream)
{
ClearScope('htmlTemplate'); //Garbage collection. Don't leave behind any listeners/watchers from the prior
//scope.
@@ -36,6 +36,8 @@ function OrganizationsList ($routeParams, $scope, $rootScope, $location, $log, R
PaginateInit({ scope: scope, list: list, url: defaultUrl });
scope.search(list.iterator);
+ scope.showActivity = function() { Stream(); }
+
scope.addOrganization = function() {
$location.path($location.path() + '/add');
}
@@ -70,7 +72,7 @@ function OrganizationsList ($routeParams, $scope, $rootScope, $location, $log, R
OrganizationsList.$inject=[ '$routeParams', '$scope', '$rootScope', '$location', '$log', 'Rest', 'Alert', 'LoadBreadCrumbs', 'Prompt',
'GenerateList', 'OrganizationList', 'SearchInit', 'PaginateInit', 'ClearScope', 'ProcessErrors',
- 'GetBasePath', 'SelectionInit', 'Wait' ];
+ 'GetBasePath', 'SelectionInit', 'Wait', 'Stream'];
function OrganizationsAdd ($scope, $rootScope, $compile, $location, $log, $routeParams, OrganizationForm,
diff --git a/awx/ui/static/js/forms/ActivityDetail.js b/awx/ui/static/js/forms/ActivityDetail.js
new file mode 100644
index 0000000000..e6a6ba16d5
--- /dev/null
+++ b/awx/ui/static/js/forms/ActivityDetail.js
@@ -0,0 +1,59 @@
+/*********************************************
+ * Copyright (c) 2013 AnsibleWorks, Inc.
+ *
+ * ActivityDetail.js
+ * Form definition for Activity Stream detail
+ *
+ */
+angular.module('ActivityDetailDefinition', [])
+ .value(
+ 'ActivityDetailForm', {
+
+ name: 'activity',
+ editTitle: 'Activity Detail',
+ well: false,
+ 'class': 'horizontal-narrow',
+
+ fields: {
+ timestamp: {
+ label: 'Time',
+ type: 'text',
+ readonly: true
+ },
+ operation: {
+ label: 'Operation',
+ type: 'text',
+ readonly: true
+ },
+ object1: {
+ label: 'Object 1',
+ type: 'text',
+ ngHide: '!object1',
+ readonly: true
+ },
+ object1_name: {
+ label: 'Name',
+ type: 'text',
+ ngHide: '!object1',
+ readonly: true
+ },
+ object2: {
+ label: 'Object 2',
+ type: 'text',
+ ngHide: '!object2',
+ readonly: true
+ },
+ object2_name: {
+ label: 'Name',
+ type: 'text',
+ ngHide: '!object2',
+ readonly: true
+ },
+ changes: {
+ label: 'Changes',
+ type: 'textarea',
+ readonly: true
+ }
+ }
+
+ }); //Form
diff --git a/awx/ui/static/js/helpers/search.js b/awx/ui/static/js/helpers/search.js
index d9ac0443cf..ecb6775ee0 100644
--- a/awx/ui/static/js/helpers/search.js
+++ b/awx/ui/static/js/helpers/search.js
@@ -125,7 +125,12 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper'])
}
else if (list.fields[fld].searchType && list.fields[fld].searchType == 'int') {
scope[iterator + 'HideSearchType'] = true;
- }
+ }
+ else if (list.fields[fld].searchType && list.fields[fld].searchType == 'isnull') {
+ scope[iterator + 'SearchType'] = 'isnull';
+ scope[iterator + 'InputDisable'] = true;
+ scope[iterator + 'SearchValue'] = 'true';
+ }
scope.search(iterator);
}
@@ -187,6 +192,16 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper'])
scope[iterator + 'SearchSelectValue'].value == null) ) {
scope[iterator + 'SearchParams'] += 'iexact=';
}
+ else if ( (list.fields[scope[iterator + 'SearchField']].searchType &&
+ (list.fields[scope[iterator + 'SearchField']].searchType == 'or')) ) {
+ scope[iterator + 'SearchParams'] = ''; //start over
+ for (var k=0; k < list.fields[scope[iterator + 'SearchField']].searchFields.length; k++) {
+ scope[iterator + 'SearchParams'] += '&or__' +
+ list.fields[scope[iterator + 'SearchField']].searchFields[k] +
+ '__icontains=' + escape(scope[iterator + 'SearchValue']);
+ }
+ scope[iterator + 'SearchParams'].replace(/^\&/,'');
+ }
else {
scope[iterator + 'SearchParams'] += scope[iterator + 'SearchType'] + '=';
}
@@ -197,9 +212,10 @@ angular.module('SearchHelper', ['RestServices', 'Utilities', 'RefreshHelper'])
scope[iterator + 'SearchParams'] += scope[iterator + 'SearchSelectValue'].value;
}
else {
- //if ( list.fields[scope[iterator + 'SearchField']].searchType == undefined ||
- // list.fields[scope[iterator + 'SearchField']].searchType == 'gtzero' ) {
- scope[iterator + 'SearchParams'] += escape(scope[iterator + 'SearchValue']);
+ if ( (list.fields[scope[iterator + 'SearchField']].searchType &&
+ (list.fields[scope[iterator + 'SearchField']].searchType !== 'or')) ) {
+ scope[iterator + 'SearchParams'] += escape(scope[iterator + 'SearchValue']);
+ }
}
scope[iterator + 'SearchParams'] += (sort_order) ? '&order_by=' + escape(sort_order) : '';
}
diff --git a/awx/ui/static/js/lists/Organizations.js b/awx/ui/static/js/lists/Organizations.js
index 964a5708c7..39c6501faa 100644
--- a/awx/ui/static/js/lists/Organizations.js
+++ b/awx/ui/static/js/lists/Organizations.js
@@ -35,6 +35,15 @@ angular.module('OrganizationListDefinition', [])
ngClick: 'addOrganization()',
"class": 'btn-success btn-xs',
awToolTip: 'Create a new organization'
+ },
+ stream: {
+ 'class': "btn-primary btn-xs activity-btn",
+ ngClick: "showActivity()",
+ awToolTip: "View Activity Stream",
+ dataPlacement: "top",
+ icon: "icon-comments-alt",
+ mode: 'all',
+ iconSize: 'large'
}
},
diff --git a/awx/ui/static/js/lists/Streams.js b/awx/ui/static/js/lists/Streams.js
index 76c023ede5..5fd025cd08 100644
--- a/awx/ui/static/js/lists/Streams.js
+++ b/awx/ui/static/js/lists/Streams.js
@@ -19,23 +19,45 @@ angular.module('StreamListDefinition', [])
"class": "table-condensed",
fields: {
+ timestamp: {
+ label: 'Event Time',
+ key: true,
+ desc: true,
+ noLink: true,
+ searchable: false
+ },
user: {
label: 'User',
- linkTo: "\{\{ activity.userLink \}\}",
+ ngBindHtml: 'activity.user',
sourceModel: 'user',
sourceField: 'username',
awToolTip: "\{\{ userToolTip \}\}",
dataPlacement: 'top'
},
- timestamp: {
- label: 'Event Time',
- },
objects: {
label: 'Objects',
- ngBindHtml: 'activity.objects'
+ ngBindHtml: 'activity.objects',
+ sortField: "object1__name,object2__name",
+ searchable: false
+ },
+ object_name: {
+ label: 'Object name',
+ searchOnly: true,
+ searchType: 'or',
+ searchFields: ['object1__name', 'object2__name']
},
description: {
- label: 'Description'
+ label: 'Description',
+ ngBindHtml: 'activity.description',
+ nosort: true,
+ searchable: false
+ },
+ system_event: {
+ label: 'System event?',
+ searchOnly: true,
+ searchType: 'isnull',
+ sourceModel: 'user',
+ sourceField: 'username'
}
},
@@ -61,5 +83,14 @@ angular.module('StreamListDefinition', [])
},
fieldActions: {
+ edit: {
+ label: 'View',
+ ngClick: "showDetail(\{\{ activity.id \}\})",
+ icon: 'icon-zoom-in',
+ "class": 'btn-default btn-xs',
+ awToolTip: 'View event details',
+ dataPlacement: 'top'
+ }
}
+
});
\ No newline at end of file
diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js
index e69aea74bb..f3fd56ec3f 100644
--- a/awx/ui/static/js/widgets/Stream.js
+++ b/awx/ui/static/js/widgets/Stream.js
@@ -27,7 +27,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti
// Try not to overlap footer. Because stream is positioned absolute, the parent
// doesn't resize correctly when stream is loaded.
- $('#tab-content-container').css({ 'min-height': stream.height() });
+ $('#tab-content-container').css({ 'min-height': stream.height() + 50 });
// Slide in stream
stream.show('slide', {'direction': 'left'}, {'duration': 500, 'queue': false });
@@ -35,9 +35,10 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti
}
}])
- .factory('HideStream', [ 'ClearScope', function(ClearScope) {
+ .factory('HideStream', [ function() {
return function() {
// Remove the stream widget
+
var stream = $('#stream-container');
stream.hide('slide', {'direction': 'left'}, {'duration': 500, 'queue': false });
@@ -54,16 +55,140 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti
}
}])
+ .factory('FixUrl', [ function() {
+ return function(u) {
+ return u.replace(/\/api\/v1\//,'/#/');
+ }
+ }])
+
+ .factory('BuildUrl', [ function() {
+ return function(obj) {
+ var url = '/#/';
+ switch(obj.base) {
+ case 'group':
+ case 'host':
+ url += 'home/' + obj.base + 's/?name=' + obj.name;
+ break;
+ case 'inventory':
+ url += 'inventories/' + obj.id;
+ break;
+ default:
+ url += obj.base + 's/' + obj.id;
+ }
+ return url;
+ }
+ }])
+
+ .factory('BuildDescription', ['FixUrl', 'BuildUrl', function(FixUrl, BuildUrl) {
+ return function(activity) {
+ var descr = '';
+ if (activity.summary_fields.user) {
+ // this is a user transaction
+ var usr = FixUrl(activity.related.user);
+ descr += 'User ' + activity.summary_fields.user.username + ' ';
+ }
+ else {
+ descr += 'System ';
+ }
+ descr += activity.operation;
+ descr += (/e$/.test(activity.operation)) ? 'd ' : 'ed ';
+ if (activity.summary_fields.object2) {
+ descr += activity.summary_fields.object2.base + ' '
+ + activity.summary_fields.object2.name + '' + [ (activity.operation == 'disassociate') ? ' from ' : ' to '];
+ }
+ if (activity.summary_fields.object1) {
+ descr += activity.summary_fields.object1.base + ' '
+ + activity.summary_fields.object1.name + '';
+ }
+ return descr;
+ }
+ }])
+
+ .factory('ShowDetail', ['Rest', 'Alert', 'GenerateForm', 'ProcessErrors', 'GetBasePath', 'FormatDate', 'ActivityDetailForm',
+ function(Rest, Alert, GenerateForm, ProcessErrors, GetBasePath, FormatDate, ActivityDetailForm) {
+ return function(activity_id) {
+
+ var generator = GenerateForm;
+ var form = ActivityDetailForm;
+ var scope;
+
+ var url = GetBasePath('activity_stream') + activity_id + '/';
+
+ // Retrieve detail record and prepopulate the form
+ Rest.setUrl(url);
+ Rest.get()
+ .success( function(data, status, headers, config) {
+ // load up the form
+ var results = data;
+
+ $('#form-modal').on('show.bs.modal', function (e) {
+ $('#form-modal-body').css({
+ width:'auto', //probably not needed
+ height:'auto', //probably not needed
+ 'max-height':'100%'
+ });
+ });
+
+ //var n = results['changes'].match(/\n/g);
+ //var rows = (n) ? n.length : 1;
+ //rows = (rows < 1) ? 3 : 10;
+ form.fields['changes'].rows = 10;
+ scope = generator.inject(form, { mode: 'edit', modal: true, related: false});
+ generator.reset();
+ for (var fld in form.fields) {
+ if (results[fld]) {
+ if (fld == 'timestamp') {
+ scope[fld] = FormatDate(new Date(results[fld]));
+ }
+ else {
+ scope[fld] = results[fld];
+ }
+ }
+ }
+ if (results.summary_fields.object1) {
+ scope['object1_name'] = results.summary_fields.object1.name;
+ }
+ if (results.summary_fields.object2) {
+ scope['object2_name'] = results.summary_fields.object2.name;
+ }
+ scope['changes'] = JSON.stringify(results['changes'], null, '\t');
+ scope.formModalAction = function() {
+ $('#form-modal').modal("hide");
+ }
+ scope.formModalActionLabel = 'OK';
+ scope.formModalCancelShow = false;
+ scope.formModalInfo = false;
+ //scope.formModalHeader = results.summary_fields.project.name + ' - SCM Status';
+ $('#form-modal .btn-success').removeClass('btn-success').addClass('btn-none');
+ $('#form-modal').addClass('skinny-modal');
+ if (!scope.$$phase) {
+ scope.$digest();
+ }
+ })
+ .error( function(data, status, headers, config) {
+ $('#form-modal').modal("hide");
+ ProcessErrors(scope, data, status, form,
+ { hdr: 'Error!', msg: 'Failed to retrieve activity: ' + activity_id + '. GET status: ' + status });
+ });
+ }
+ }])
+
.factory('Stream', ['$rootScope', '$location', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', 'StreamList', 'SearchInit',
- 'PaginateInit', 'GenerateList', 'FormatDate', 'ShowStream', 'HideStream',
+ 'PaginateInit', 'GenerateList', 'FormatDate', 'ShowStream', 'HideStream', 'BuildDescription', 'FixUrl', 'ShowDetail',
function($rootScope, $location, Rest, GetBasePath, ProcessErrors, Wait, StreamList, SearchInit, PaginateInit, GenerateList,
- FormatDate, ShowStream, HideStream) {
+ FormatDate, ShowStream, HideStream, BuildDescription, FixUrl, ShowDetail) {
return function(params) {
var list = StreamList;
var defaultUrl = GetBasePath('activity_stream');
var view = GenerateList;
-
+ var base = $location.path().replace(/^\//,'').split('/')[0];
+
+ if (base !== 'home') {
+ var type = (base == 'inventories') ? 'inventory' : base.replace(/s$/,'');
+ defaultUrl += '?or__object1=' + type + '&or__object2=' + type;
+ }
+
// Push the current page onto browser histor. If user clicks back button, restore current page without
// stream widget
// window.history.pushState({}, "AnsibleWorks AWX", $location.path());
@@ -80,14 +205,16 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti
});
scope.closeStream = function() {
- HideStream();
- }
+ HideStream();
+ }
scope.refreshStream = function() {
- scope.search(list.iterator);
- }
+ scope.search(list.iterator);
+ }
- function fixUrl(u) { return u.replace(/\/api\/v1\//,'/#/'); }
+ scope.showDetail = function(id) {
+ ShowDetail(id);
+ }
if (scope.removePostRefresh) {
scope.removePostRefresh();
@@ -97,24 +224,38 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti
// Convert event_time date to local time zone
cDate = new Date(scope['activities'][i].timestamp);
scope['activities'][i].timestamp = FormatDate(cDate);
+
// Display username
scope['activities'][i].user = (scope['activities'][i].summary_fields.user) ? scope['activities'][i].summary_fields.user.username :
- 'System';
- if (scope['activities'][i].user !== 'System') {
- scope['activities'][i].userLink = (scope['activities'][i].summary_fields.user) ? fixUrl(scope['activities'][i].related.user) :
- "";
+ 'system';
+ if (scope['activities'][i].user !== 'system') {
+ // turn user into a link when not 'system'
+ scope['activities'][i].user = "" +
+ scope['activities'][i].user + "";
}
-
+
// Objects
var href;
+ var deleted = /^\_delete/;
if (scope['activities'][i].summary_fields.object1) {
- href = fixUrl(scope['activities'][i].related.object_1);
- scope['activities'][i].objects = "" + scope['activities'][i].summary_fields.object1.name + "";
+ if ( !deleted.test(scope['activities'][i].summary_fields.object1.name) ) {
+ href = FixUrl(scope['activities'][i].related.object1);
+ scope['activities'][i].objects = "" + scope['activities'][i].summary_fields.object1.name + "";
+ }
+ else {
+ scope['activities'][i].objects = scope['activities'][i].summary_fields.object1.name;
+ }
}
if (scope['activities'][i].summary_fields.object2) {
- href = fixUrl(scope['activities'][i].related.object_2);
- scope['activities'][i].objects += ", " + scope['activities'][i].summary_fields.object2.name + "";
+ if ( !deleted.test(scope['activities'][i].summary_fields.object2.name) ) {
+ href = FixUrl(scope['activities'][i].related.object2);
+ scope['activities'][i].objects += ", " + scope['activities'][i].summary_fields.object2.name + "";
+ }
+ else {
+ scope['activities'][i].objects += scope['activities'][i].summary_fields.object2.name;
+ }
}
+ scope['activities'][i].description = BuildDescription(scope['activities'][i]);
}
ShowStream();
});
@@ -122,8 +263,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti
// Initialize search and paginate pieces and load data
SearchInit({ scope: scope, set: list.name, list: list, url: defaultUrl });
PaginateInit({ scope: scope, list: list, url: defaultUrl });
- scope.search(list.iterator);
-
+ scope.search(list.iterator);
}
}]);
\ No newline at end of file
diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html
index e3d5e6378a..28b6ce8dc5 100644
--- a/awx/ui/templates/ui/index.html
+++ b/awx/ui/templates/ui/index.html
@@ -75,6 +75,7 @@
+